require 'command_line'

module DistributedTestRunner

  class AveragePacker
    def pack(test_files, packet_amount)
      @packets = []
      size = (test_files.size.to_f / packet_amount).ceil
      size = 1 if size <= 0
      @packets << test_files.slice!(0, size) until test_files.empty?
      @packets
    end
  end
  
  class Server
    
    attr_accessor :test_files, :packer, :runner_name
    
    def %
      return '0%' if test_files.nil? || test_files.empty?
      "#{reports.size/test_files.size * 100}%"
    end
    
    def latest_build_summary
      return nil unless defined? @build_time
      %{Finished in #{format('%.1f', @build_time)} seconds.

#{collect_report(:tests)}, #{collect_report(:assertions)}, #{collect_report(:failures)}, #{collect_report(:errors)}}
    end

    def clients
      @clients ||= []
    end

    def succeeded?
      complete? && reports.values.all?{|report| report.succeeded?}
    end
    
    def complete?
      @complete ||= false
    end
    
    def run(opt={:wait_for_reports => true})
      @signature = (build_start_time = Time.now).to_s
      
      loop do
        self.clients.select{|client| client.idle?}.each do |client|
          if @packets.nil? || @packets.empty?
            no_report_tests = test_files - reports.keys
            @packets = packer.pack no_report_tests, clients.size
          end
          client.run self.runner_name, @packets.pop, @signature
        end
        break if !opt[:wait_for_reports] || complete?
      end
      @build_time = (Time.now - build_start_time)/1000
    end
    
    def reports
      @reports ||= {}
    end
    
    def update(signature, report)
      return if @signature.nil? || signature != @signature
      self.reports[report.test] = report
      @signature = nil if @complete = self.reports.keys.size == @test_files.size
    end

    private
    def collect_report(type)
      "#{reports.values.collect{|r| r.send(type)}.inject(0){|r, i| r += i}} #{type}"
    end
  end

  class RubyRunner
    def run(server, client, test_files, signature)
      test_files.each do |test_file|
        name, stdout, stderr, exit_code = client.execute("ruby #{test_file}")
        tests, assertions, failures, errors = parse(stdout)
        server.update signature, Report.new(test_file, name, stdout, stderr, exit_code, tests, assertions, failures, errors)
      end
    end

    def parse(stdout)
      summary = stdout.strip.split("\n").last.strip
      if /^(\d+) tests, (\d+) assertions, (\d+) failures, (\d+) errors$/ =~ summary
        [$1, $2, $3, $4].collect{|str| str.to_i}
      else
        raise "stdout no tests summary line: #{stdout}"
      end
    end
  end

  class Report
    attr_reader :test, :stdout, :stderr, :exit_code, :client_name, :tests, :assertions, :failures, :errors
    def initialize(test, client_name, stdout, stderr, exit_code, tests, assertions, failures, errors)
      @test = test
      @client_name = client_name
      @stdout = stdout
      @stderr = stderr
      @exit_code = exit_code
      @tests = tests
      @assertions = assertions
      @failures = failures
      @errors = errors
    end

    def succeeded?
      @failures == 0 && @errors == 0
    end

    def failed?
      @failures > 0
    end

    def error?
      @errors > 0
    end

    def ==(another)
      test == another.test
    end
  end
  
  class Client
    
    RUNNERS = {RubyRunner.name => RubyRunner.new}
    
    attr_reader :name

    def initialize(server, name)
      @server = server
      @name = name
    end
    
    def idle?
      @executing_cmd.nil?
    end
    
    def run(runner_name, test_files, signature)
      RUNNERS[runner_name].run(@server, self, test_files, signature)
    end
    
    def completed_cmds
      @completed_cmds ||= []
    end

    def execute(cmd)
      @executing_cmd = cmd
      outputs = {:stdout => 'stdout.log', :stderr => 'stderr.log'}
      outputs.values.each do |log_file|
        File.delete log_file if File.exist? log_file
      end

      begin
        CommandLine.execute cmd, outputs
        exit_code = 0
      rescue CommandLine::ExecutionError => e
        exit_code = e.exitstatus
      end

      outputs.each_key do |key|
        outputs[key] = File.open(outputs[key], 'r') {|f| f.read}
      end

      self.completed_cmds << cmd
      @executing_cmd = nil
      [@name, outputs[:stdout], outputs[:stderr], exit_code]
    end
  end
end
